在上一篇文章中,我們介紹了Stream API的基本概念和用法。今天,我們將深入探討Stream API的高級特性,並通過實際案例來展示其在複雜場景中的應用。
Stream API是Java 8引入的一個用於處理數據序列的工具,允許以聲明式方式處理集合,並支持函數式風格。
Stream的操作可分為中間操作(如filter、map)和終端操作(如collect、reduce)。
以下針對昨天,再補充部分內容
終端操作會遍歷流並產生一個結果。執行終端操作後,流就被消費掉了。常見的終端操作包括:
collect:將流元素收集到一個容器中
List<String> list = stream.collect(Collectors.toList());
forEach:對每個元素執行操作
stream.forEach(System.out::println);
reduce:將流元素組合起來
Optional<String> combined = stream.reduce((s1, s2) -> s1 + s2);
count:計算流中的元素個數
long count = stream.count();
短路操作可以在不處理所有元素的情況下返回結果。 常見的短路操作包括:
findFirst:返回第一個元素
Optional<String> first = stream.findFirst();
findAny:返回任意一個元素
Optional<String> any = stream.findAny();
anyMatch:檢查是否至少有一個元素匹配給定的條件
boolean hasA = stream.anyMatch(s -> s.startsWith("a"));
allMatch:檢查是否所有元素都匹配給定的條件
boolean allLong = stream.allMatch(s -> s.length() > 5);
noneMatch:檢查是否沒有元素匹配給定的條件
boolean noShort = stream.noneMatch(s -> s.length() < 3);
複雜條件過濾:
List<Person> adults = people.stream()
.filter(p -> p.getAge() >= 18 && p.getCountry().equals("Taiwan"))
.collect(Collectors.toList());
數據轉換和扁平化:
List<List<String>> nestedList = Arrays.asList(
Arrays.asList("a", "b"),
Arrays.asList("c", "d")
);
List<String> flatList = nestedList.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
// Output:[a, b, c, d]
Stream API適合進行數據彙總操作:
求和:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
// Output:15
查找最大值:
Optional<Integer> max = numbers.stream()
.max(Integer::compareTo);
// 結果:Optional[5]
分組統計:
Map<String, Long> countByCountry = people.stream()
.collect(Collectors.groupingBy(Person::getCountry, Collectors.counting()));
public class CustomCollectorExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
String result = names.stream().collect(Collector.of(
StringBuilder::new,
(sb, str) -> {
if (sb.length() > 0) sb.append(", ");
sb.append(str);
},
StringBuilder::append,
StringBuilder::toString
));
System.out.println(result); // Output: Alice, Bob, Charlie, David
}
}
Spliterator是Java 8引入的另一個重要概念,是Iterator的可分割版本,為並行處理提供了更好的解法。
public class SpliteratorExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Spliterator<String> spliterator = names.spliterator();
spliterator.forEachRemaining(name -> System.out.println("Processing: " + name));
}
}
Optional可以與Stream無縫結合,提供更安全和優雅的空值處理。
public class StreamOptionalExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Optional<String> firstLongName = names.stream()
.filter(name -> name.length() > 5)
.findFirst();
firstLongName.ifPresent(System.out::println);
}
}
CompletableFuture可以與Stream結合使用,實現高效的異步處理。
public class StreamCompletableFutureExample {
public static void main(String[] args) {
List<String> urls = Arrays.asList("url1", "url2", "url3");
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> fetchUrl(url)))
.collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
List<String> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
System.out.println(results);
}
private static String fetchUrl(String url) {
// 模擬網絡請求
return "Content of " + url;
}
}
平行Stream是Java 8引入的一個強大特性,利用多核處理器的優勢,並行處理Stream中的元素,從而提高處理大量數據時的效能。在本章節中,我們將探討什麼是平行Stream,如何創建和使用他,以及使用時需要注意的事項。
創建平行Stream有兩種主要方式:
從現有的集合創建:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Stream<Integer> parallelStream = numbers.parallelStream();
將順序Stream轉換為平行Stream:
Stream<Integer> parallelStream = numbers.stream().parallel();
使用平行Stream的方式與普通Stream相同,只是處理過程會自動並行化:
int sum = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
提高效能:對於大量數據的處理,平行Stream可以顯著提高處理速度。
簡化並行編程:無需手動管理執行緒,Java運行時會自動處理並行化。
適應性強:同一段程式碼可以在不同核心數的處理器上自動調整並行度。
數據量要足夠大:對於小型集合,平行化的開銷可能會超過其帶來的效能提升。
避免使用有狀態的Lambda表達式:並行處理時,有狀態的操作可能導致不可預期的結果。
注意執行緒安全:如果在平行Stream操作中修改共享狀態,需要確保執行緒安全。
考慮合併成本:某些操作(如排序)在並行執行後需要合併結果,這可能會抵消並行化帶來的效能提升。
測試效能:並不是所有情況下平行Stream都比順序Stream快,需要根據實際情況進行測試。
// 效能測試範例
long start = System.currentTimeMillis();
int sum = numbers.parallelStream().sum();
long end = System.currentTimeMillis();
System.out.println("Parallel execution time: " + (end - start) + "ms");
start = System.currentTimeMillis();
sum = numbers.stream().sum();
end = System.currentTimeMillis();
System.out.println("Sequential execution time: " + (end - start) + "ms");
平行Stream,可以幫助我們充分利用現代多核處理器的優勢,但並不是萬能的解決方案,我們需要謹慎考慮數據特性、操作複雜度以及硬體環境等因素,以確保能夠真正獲得效能提升。
Stream的一個重要特性是惰性求值(lazy evaluation),這意味著中間操作不會立即執行,而是等到遇到終端操作時才會觸發,理解這一點對於優化Stream性能至關重要。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Stream<String> stream = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase);
// 此時還沒有執行任何操作
List<String> result = stream.collect(Collectors.toList());
// 只有在這裡,之前的操作才會真正執行
利用惰性求值的特性,我們可以:
在處理基本數據類型時,使用專門的Stream可以避免不必要的裝箱和拆箱操作,從而提高性能。
// 避免使用
Stream<Integer> boxedStream = IntStream.range(1, 1000000).boxed();
// 推薦使用
IntStream primitiveStream = IntStream.range(1, 1000000);
選擇合適的終端操作可以顯著影響Stream的性能:
// 較慢
int sum = numbers.stream().reduce(0, Integer::sum);
// 較快
int sum = numbers.stream().mapToInt(Integer::intValue).sum();
並行Stream不總是能提高性能。在以下情況下,並行Stream可能會帶來更好的性能:
在使用並行Stream時也要注意:
如果需要多次處理同一數據集,考慮重用Stream的源,而不是重複創建Stream。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Supplier<Stream<String>> streamSupplier = names::stream;
long count = streamSupplier.get().filter(name -> name.length() > 4).count();
List<String> filtered = streamSupplier.get().filter(name -> name.length() > 4).collect(Collectors.toList());
處理大量數據:Stream API特別適合處理大量數據,尤其是當需要進行複雜的轉換、過濾或彙總操作時。
函數式操作:當您的操作可以用函數式的方式表達時,Stream API可以提供更簡潔的解決方案。
鏈式操作:如果您需要進行一系列的數據處理步驟,Stream的鏈式調用可以提高程式碼的可讀性。
List<String> result = people.stream()
.filter(p -> p.getAge() > 18)
.map(Person::getName)
.sorted()
.limit(10)
.collect(Collectors.toList());
// 適度使用方法引用
List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
// 有時Lambda表達式更清晰
List<String> upperNames = people.stream()
.map(person -> person.getName().toUpperCase())
.collect(Collectors.toList());
List<String> result = people.stream()
.filter(p -> p.getAge() > 18)
.map(Person::getName)
.sorted()
.limit(10)
.collect(Collectors.toList());
Stream<Person> adultStream = people.stream().filter(p -> p.getAge() > 18);
Stream<String> nameStream = adultStream.map(Person::getName);
List<String> sortedNames = nameStream.sorted().collect(Collectors.toList());
// 不推薦
List<String> names = new ArrayList<>();
people.stream().forEach(p -> names.add(p.getName()));
// 推薦
List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
// 不推薦
Map<String, Integer> ageByName = new HashMap<>();
people.stream().forEach(p -> ageByName.put(p.getName(), p.getAge()));
// 推薦
Map<String, Integer> ageByName = people.stream()
.collect(Collectors.toMap(Person::getName, Person::getAge));
// 執行緒不安全
List<String> names = Collections.synchronizedList(new ArrayList<>());
people.parallelStream().forEach(p -> names.add(p.getName()));
// 執行緒安全
List<String> names = people.parallelStream()
.map(Person::getName)
.collect(Collectors.toList());
遵循這些實踐,可以幫助我們更有效地利用Stream API,寫出更加簡潔、高效、易於維護的程式碼。在下一章節中,我們將探討Stream API的一些限制和替代方案,幫助您在不同場景下做出選擇。
雖然Stream API為Java程式設計帶來許多便利,但他並非萬能的解決方案。在某些情況下,Stream API可能會遇到限制,或者其他方法可能更為合適。本章節將探討Stream API的一些局限性,以及何時應該考慮使用替代方案。
性能開銷:對於小型集合或簡單操作,Stream API可能引入不必要的性能開銷。
調試困難:由於Stream操作的鏈式調用和惰性求值特性,調試Stream相關的程式碼可能會比較困難。
有限的重用性:Stream只能被消費一次,這可能導致程式碼重複或效率降低。
不支持受檢異常:Stream操作中不能拋出受檢異常,這可能導致錯誤處理變得複雜。
// 使用Stream
List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
// 使用傳統for循環
List<String> names = new ArrayList<>();
for (Person person : people) {
names.add(person.getName());
}
需要break或continue的場景:Stream API不支持break或continue操作,在這些場景下傳統循環更合適。
需要修改外部變量:雖然技術上可行,但在Stream操作中修改外部變量違背函數式編程的原則,也可能導致並發問題。
處理基本類型數組:雖然Stream API提供專門的IntStream、LongStream等,但在某些情況下,直接操作數組可能更高效。
for (int i = 0; i < array.length; i++) {
if (someCondition) {
// 可以使用break或continue
break;
}
// 可以自由修改外部變量
}
for (Person person : people) {
System.out.println(person.getName());
}
Iterator<Person> iterator = people.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
if (person.getAge() < 18) {
iterator.remove();
}
}
// 使用Guava的Iterables類
Iterable<String> names = Iterables.transform(people, Person::getName);
假設我們有一個電子商務平台,需要處理大量的訂單數據。我們的任務是找出最近30天內,金額超過1000元的訂單,按照金額降序排列,並返回前10個訂單的客戶名稱。
首先,讓我們看看使用傳統方法如何實現這個需求:
public List<String> getTop10CustomerNames(List<Order> orders) {
List<Order> filteredOrders = new ArrayList<>();
Date thirtyDaysAgo = Date.from(Instant.now().minus(30, ChronoUnit.DAYS));
// 過濾訂單
for (Order order : orders) {
if (order.getDate().after(thirtyDaysAgo) && order.getAmount() > 1000) {
filteredOrders.add(order);
}
}
// 排序訂單
Collections.sort(filteredOrders, (o1, o2) -> Double.compare(o2.getAmount(), o1.getAmount()));
// 獲取前10個客戶名稱
List<String> customerNames = new ArrayList<>();
for (int i = 0; i < Math.min(10, filteredOrders.size()); i++) {
customerNames.add(filteredOrders.get(i).getCustomerName());
}
return customerNames;
}
現在,讓我們看看如何使用Stream API來實現相同的功能:
public List<String> getTop10CustomerNamesWithStream(List<Order> orders) {
Date thirtyDaysAgo = Date.from(Instant.now().minus(30, ChronoUnit.DAYS));
return orders.stream()
.filter(order -> order.getDate().after(thirtyDaysAgo) && order.getAmount() > 1000)
.sorted(Comparator.comparingDouble(Order::getAmount).reversed())
.limit(10)
.map(Order::getCustomerName)
.collect(Collectors.toList());
}
假設我們需要對訂單系統進行更複雜的數據分析,例如按類別統計訂單金額:
public class OrderAnalysis {
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("A001", 100.0, "Electronics"),
new Order("A002", 200.0, "Books"),
new Order("A003", 300.0, "Electronics"),
new Order("A004", 150.0, "Clothing")
);
Map<String, DoubleSummaryStatistics> statisticsByCategory = orders.stream()
.collect(Collectors.groupingBy(
Order::getCategory,
Collectors.summarizingDouble(Order::getAmount)
));
statisticsByCategory.forEach((category, stats) -> {
System.out.println(category + " - Average: " + stats.getAverage() +
", Max: " + stats.getMax() +
", Min: " + stats.getMin());
});
}
}
class Order {
private String id;
private double amount;
private String category;
// Constructor, getters and setters
}
程式碼簡潔性:
可維護性:
性能:
靈活性:
錯誤處理:
本篇文章同步刊載: JYI.TW
筆者個人的網站: JUNYI